/*eslint-env amd */
define([
	'i18n!javascript/nls/problems',
	'module'
], function(ProblemMessages, module) {
	/**
	 * @fileoverview Rule to flag use of an object property of the global object (Math and JSON) as a function
	 * @author James Allardice
	 */

	"use strict";

	//--------------------------------------------------------------------------
	// Helpers
	//--------------------------------------------------------------------------

	/**
	 * Determines whether an operator is a comparison operator.
	 * @param {String} operator The operator to check.
	 * @returns {boolean} Whether or not it is a comparison operator.
	 */
	function isComparisonOperator(operator) {
		return (/^(==|===|!=|!==|<|>|<=|>=)$/).test(operator);
	}

	/**
	 * Determines whether an operator is an equality operator.
	 * @param {String} operator The operator to check.
	 * @returns {boolean} Whether or not it is an equality operator.
	 */
	function isEqualityOperator(operator) {
		return (/^(==|===)$/).test(operator);
	}

	/**
	 * Determines whether an operator is one used in a range test.
	 * Allowed operators are `<` and `<=`.
	 * @param {String} operator The operator to check.
	 * @returns {boolean} Whether the operator is used in range tests.
	 */
	function isRangeTestOperator(operator) {
		return ["<", "<="].indexOf(operator) >= 0;
	}

	/**
	 * Determines whether a non-Literal node is a negative number that should be
	 * treated as if it were a single Literal node.
	 * @param {ASTNode} node Node to test.
	 * @returns {boolean} True if the node is a negative number that looks like a
	 *                    real literal and should be treated as such.
	 */
	function looksLikeLiteral(node) {
		return node.type === "UnaryExpression" &&
			node.operator === "-" &&
			node.prefix &&
			node.argument.type === "Literal" &&
			typeof node.argument.value === "number";
	}

	/**
	 * Attempts to derive a Literal node from nodes that are treated like literals.
	 * @param {ASTNode} node Node to normalize.
	 * @returns {ASTNode} The original node if the node is already a Literal, or a
	 *                    normalized Literal node with the negative number as the
	 *                    value if the node represents a negative number literal,
	 *                    otherwise null if the node cannot be converted to a
	 *                    normalized literal.
	 */
	function getNormalizedLiteral(node) {
		if (node.type === "Literal") {
			return node;
		}

		if (looksLikeLiteral(node)) {
			return {
				type: "Literal",
				value: -node.argument.value,
				raw: "-" + node.argument.value
			};
		}

		return null;
	}

	/**
	 * Checks whether two expressions reference the same value. For example:
	 *     a = a
	 *     a.b = a.b
	 *     a[0] = a[0]
	 *     a['b'] = a['b']
	 * @param   {ASTNode} a Left side of the comparison.
	 * @param   {ASTNode} b Right side of the comparison.
	 * @returns {boolean}   True if both sides match and reference the same value.
	 */
	function same(a, b) {
		if (a.type !== b.type) {
			return false;
		}

		switch (a.type) {
			case "Identifier":
				return a.name === b.name;

			case "Literal":
				return a.value === b.value;

			case "MemberExpression":

				// x[0] = x[0]
				// x[y] = x[y]
				// x.y = x.y
				return same(a.object, b.object) && same(a.property, b.property);

			case "ThisExpression":
				return true;

			default:
				return false;
		}
	}

	//------------------------------------------------------------------------------
	// Rule Definition
	//------------------------------------------------------------------------------

	module.exports = function(context) {

		// Default to "never" (!always) if no option
		var always = context.options[0] === "always";
		var exceptRange = context.options[1] && context.options[1].exceptRange;
		var onlyEquality = context.options[1] && context.options[1].onlyEquality;

		var sourceCode = context.getSourceCode();

		/**
		 * Determines whether node represents a range test.
		 * A range test is a "between" test like `(0 <= x && x < 1)` or an "outside"
		 * test like `(x < 0 || 1 <= x)`. It must be wrapped in parentheses, and
		 * both operators must be `<` or `<=`. Finally, the literal on the left side
		 * must be less than or equal to the literal on the right side so that the
		 * test makes any sense.
		 * @param {ASTNode} node LogicalExpression node to test.
		 * @returns {Boolean} Whether node is a range test.
		 */
		function isRangeTest(node) {
			var left = node.left,
				right = node.right;

			/**
			 * Determines whether node is of the form `0 <= x && x < 1`.
			 * @returns {Boolean} Whether node is a "between" range test.
			 */
			function isBetweenTest() {
				var leftLiteral, rightLiteral;

				return node.operator === "&&" &&
					(leftLiteral = getNormalizedLiteral(left.left)) &&
					(rightLiteral = getNormalizedLiteral(right.right)) &&
					leftLiteral.value <= rightLiteral.value &&
					same(left.right, right.left);
			}

			/**
			 * Determines whether node is of the form `x < 0 || 1 <= x`.
			 * @returns {Boolean} Whether node is an "outside" range test.
			 */
			function isOutsideTest() {
				var leftLiteral, rightLiteral;

				return node.operator === "||" &&
					(leftLiteral = getNormalizedLiteral(left.right)) &&
					(rightLiteral = getNormalizedLiteral(right.left)) &&
					leftLiteral.value <= rightLiteral.value &&
					same(left.left, right.right);
			}

			/**
			 * Determines whether node is wrapped in parentheses.
			 * @returns {Boolean} Whether node is preceded immediately by an open
			 *                    paren token and followed immediately by a close
			 *                    paren token.
			 */
			function isParenWrapped() {
				var tokenBefore, tokenAfter;

				return (tokenBefore = sourceCode.getTokenBefore(node)) &&
					tokenBefore.value === "(" &&
					(tokenAfter = sourceCode.getTokenAfter(node)) &&
					tokenAfter.value === ")";
			}

			return node.type === "LogicalExpression" &&
				left.type === "BinaryExpression" &&
				right.type === "BinaryExpression" &&
				isRangeTestOperator(left.operator) &&
				isRangeTestOperator(right.operator) &&
				(isBetweenTest() || isOutsideTest()) &&
				isParenWrapped();
		}

		//--------------------------------------------------------------------------
		// Public
		//--------------------------------------------------------------------------

		return {
			BinaryExpression: always ? function(node) {

				// Comparisons must always be yoda-style: if ("blue" === color)
				if (
					(node.right.type === "Literal" || looksLikeLiteral(node.right)) &&
					!(node.left.type === "Literal" || looksLikeLiteral(node.left)) &&
					!(!isEqualityOperator(node.operator) && onlyEquality) &&
					isComparisonOperator(node.operator) &&
					!(exceptRange && isRangeTest(context.getAncestors().pop()))
				) {
					context.report(node, ProblemMessages.yodaLeft, {operator: node.operator});
				}

			} : function(node) {

				// Comparisons must never be yoda-style (default)
				if (
					(node.left.type === "Literal" || looksLikeLiteral(node.left)) &&
					!(node.right.type === "Literal" || looksLikeLiteral(node.right)) &&
					!(!isEqualityOperator(node.operator) && onlyEquality) &&
					isComparisonOperator(node.operator) &&
					!(exceptRange && isRangeTest(context.getAncestors().pop()))
				) {
					context.report(node, ProblemMessages.yodaRight, {operator: node.operator});
				}

			}
		};

	};

	module.exports.schema = [{
		"enum": ["always", "never"]
	}, {
		type: "object",
		properties: {
			exceptRange: {
				type: "boolean"
			},
			onlyEquality: {
				type: "boolean"
			}
		},
		additionalProperties: false
	}];

	return module.exports;
});